/*
 * @Author: 张淑滨
 * @Date: 2023-12-05 13:55:09
 * @Last Modified time: 2023-12-05 13:55:09
 */
import { mxCell, mxCellState, mxMouseEvent, mxUndoManager as mUndoManager } from 'mxgraph'
import GraphUtil from '../util/GraphUtil'
import '@/mxgraph'
import { Graph as mGraph, HoverIcons as HIcons } from '../types'
import defaultTheme from '../shape/themes/default.xml?raw'
import DefultStencils from '@/shape/stencils'
import { initPopupMenu } from './PopupMenu'
import { message } from 'antd'
// const { mxGraph, mxEvent, mxVertexHandler, mxConstants } = mx

interface IEditorProps {
  container: string | Element
  clickFunc?: (cell: mxCell) => void
  /**
   * cell 变化时的回调
   * @param cells
   * @returns
   */
  changeFunc?: (cells: mxCell[]) => void
  doubleClickFunc?: () => void
  autoSaveFunc?: () => void
  deleteFunc?: () => void
  undoFunc?: () => void
  copyFunc?: () => void
  cellCreatedFunc?: (cell: mxCell) => void
  valueChangeFunc?: () => void
}

export default class Editor {
  graph: mGraph
  hoverIcons: HIcons
  undoManager: mUndoManager
  constructor(props: IEditorProps) {
    const { container } = props

    let containerEle
    if (typeof container === 'string') {
      containerEle = document.querySelector(container)
    } else {
      containerEle = container
    }
    //生成一个初始化过的Graph对象
    //@ts-ignore
    this.graph = this.createGraph(containerEle)
    // 设置graph的默认属性
    this.resetGraph()
    // 加载素材
    this.loadShapes()
    // 配置右键菜单
    initPopupMenu(this.graph)
    // 配置连接提示
    this.hoverIcons = new HoverIcons(this.graph)
    // 撤销重做事件管理
    this.undoManager = this.createUndoManager(this.graph)
    // 注册画布事件
    this.registerGraphEvent(this.graph, {
      ...props
    })
  }

  /**
   * 生成一个初始化过的Graph对象
   * @param container
   * @returns
   */
  createGraph(container: Element) {
    const graph = new Graph(container)
    // 对graph进行扩展
    this.extendGraph(graph)
    //加载样式
    GraphUtil.setStylesheet(graph, defaultTheme)
    // 启用拖拉选择
    new mxRubberband(graph)
    // Disables the built-in context menu
    mxEvent.disableContextMenu(container) // eslint-disable-line
    mxVertexHandler.prototype.rotationEnabled = true // eslint-disable-line
    return graph
  }

  /**
   * 为画布注册事件
   * @param graph
   * @param config
   */
  registerGraphEvent(graph: mGraph, config: any) {
    // eslint-disable-line
    const {
      clickFunc,
      doubleClickFunc,
      autoSaveFunc,
      hoverFunc,
      deleteFunc,
      undoFunc,
      copyFunc,
      valueChangeFunc,
      changeFunc
    } = config

    // Uncomment the following if you want the container
    // to fit the size of the graph
    // graph.setResizeContainer(true);

    GraphUtil.initZoomConfig({ graph })

    // undo event listener
    GraphUtil.undoListener({
      graph,
      callback: undoFunc
    })

    // copy event listener
    GraphUtil.copyListener({
      graph,
      callback: copyFunc
    })

    // delete event listener
    GraphUtil.deleteListener({
      graph,
      callback: deleteFunc
    })

    // connector handler
    GraphUtil.connectorHandler({
      graph
    })

    GraphUtil.handleDoubleClick({
      graph,
      callback: doubleClickFunc
    })

    GraphUtil.handleClick({
      graph,
      callback: clickFunc
    })

    GraphUtil.handleHover({
      graph,
      callback: hoverFunc
    })

    GraphUtil.handleChange({
      graph,
      callback: changeFunc
    })

    GraphUtil.initAutoSave({
      graph,
      callback: autoSaveFunc
    })

    GraphUtil.vertexRenameListener({
      callback: valueChangeFunc
    })
  }

  /**
   * 加载形状素材
   */
  loadShapes() {
    // 加载js素材定义, 目前通过GraphUtil中引入GeneralShape引入实现
    // import '../shape/GeneralShape'
    // 加载Stencils素材
    GraphUtil.loadStencils(DefultStencils)
  }

  /**
   * 扩展graph实例上的方法
   * @param graph
   */
  extendGraph(graph: any) {
    /**
     * Returns the padding for pages in page view with scrollbars.
     */
    graph.getPagePadding = function () {
      return new mxPoint(
        Math.max(0, Math.round((graph.container.offsetWidth - 34) / graph.view.scale)),
        Math.max(0, Math.round((graph.container.offsetHeight - 34) / graph.view.scale))
      )
    }

    // Fits the number of background pages to the graph
    graph.view.getBackgroundPageBounds = function () {
      const layout = this.graph.getPageLayout()
      const page = this.graph.getPageSize()

      return new mxRectangle(
        this.scale * (this.translate.x + layout.x * page.width),
        this.scale * (this.translate.y + layout.y * page.height),
        this.scale * layout.width * page.width,
        this.scale * layout.height * page.height
      )
    }

    graph.getPreferredPageSize = function () {
      const pages = this.getPageLayout()
      const size = this.getPageSize()

      return new mxRectangle(0, 0, pages.width * size.width, pages.height * size.height)
    }

    graph.sizeDidChange = function () {
      if (this.container != null && mxUtils.hasScrollbars(this.container)) {
        const pages = this.getPageLayout()
        const pad = this.getPagePadding()
        const size = this.getPageSize()

        // Updates the minimum graph size
        const minw = Math.ceil(2 * pad.x + pages.width * size.width)
        const minh = Math.ceil(2 * pad.y + pages.height * size.height)

        const min = this.minimumGraphSize

        // LATER: Fix flicker of scrollbar size in IE quirks mode
        // after delayed call in window.resize event handler
        if (min == null || min.width != minw || min.height != minh) {
          this.minimumGraphSize = new mxRectangle(0, 0, minw, minh)
        }

        // Updates auto-translate to include padding and graph size
        const dx = pad.x - pages.x * size.width
        const dy = pad.y - pages.y * size.height

        if (!this.autoTranslate && (this.view.translate.x != dx || this.view.translate.y != dy)) {
          this.autoTranslate = true
          this.view.x0 = pages.x
          this.view.y0 = pages.y

          // NOTE: THIS INVOKES THIS METHOD AGAIN. UNFORTUNATELY THERE IS NO WAY AROUND THIS SINCE THE
          // BOUNDS ARE KNOWN AFTER THE VALIDATION AND SETTING THE TRANSLATE TRIGGERS A REVALIDATION.
          // SHOULD MOVE TRANSLATE/SCALE TO VIEW.
          const tx = graph.view.translate.x
          const ty = graph.view.translate.y
          graph.view.setTranslate(dx, dy)

          // LATER: Fix rounding errors for small zoom
          graph.container.scrollLeft += Math.round((dx - tx) * graph.view.scale)
          graph.container.scrollTop += Math.round((dy - ty) * graph.view.scale)

          this.autoTranslate = false

          return
        }
        //@ts-ignore
        // eslint-disable-next-line prefer-rest-params
        mxGraph.prototype.sizeDidChange.apply(this, arguments)
      } else {
        // Fires event but does not invoke superclass
        //this.fireEvent(new mxEventObject(mxEvent.SIZE, 'bounds', this.getGraphBounds()))
      }
    }
    graph.view.validate = function () {
      if (this.graph.container != null && mxUtils.hasScrollbars(this.graph.container)) {
        const pad = this.graph.getPagePadding()
        const size = this.graph.getPageSize()

        // Updating scrollbars here causes flickering in quirks and is not needed
        // if zoom method is always used to set the current scale on the graph.
        const tx = this.translate.x
        const ty = this.translate.y
        this.translate.x = pad.x - (this.x0 || 0) * size.width
        this.translate.y = pad.y - (this.y0 || 0) * size.height

        // console.log('set translate.x')
      }
      //@ts-ignore
      // eslint-disable-next-line prefer-rest-params
      mxGraphView.prototype.validate.apply(this, arguments)
    }

    /**
     * 设置标签的提示
     * @param state
     * @param node
     * @param x
     * @param y
     * @returns
     */
    graph.getTooltip = function (state: mxCellState, node: Node, x: number, y: number) {
      const cell = state.cell
      const model = this.getModel()

      if (model.isVertex(cell)) {
        // 如果是测点数据
        const v = cell.value
        if (mxUtils.isNode(v, 'object')) {
          const name = (v as Element).getAttribute('type')
          const code = (v as Element).getAttribute('deviceCode')
          const dataName = (v as Element).getAttribute('dataName')
          if (name == 'HDataPoint') {
            return `设备编号: ${code ? code : '--'}
            绑定变量：${dataName ? dataName : '--'}`
          }
        }
        return this.getLabel(cell)
      }
    }
  }

  createUndoManager(graph: any) {
    const undoManager = new mxUndoManager() //eslint-disable-line

    graph.undoManager = undoManager

    const listener = (sender: any, evt: any) => {
      undoManager.undoableEditHappened(evt.getProperty('edit'))
    }
    graph.getModel().addListener(mxEvent.UNDO, listener) //eslint-disable-line
    graph.getView().addListener(mxEvent.UNDO, listener) //eslint-disable-line
    return undoManager
  }

  /**
   * update style
   * @param {*} cell cell
   * @param {*} key the key of style
   * @param {*} value the value of style
   */
  updateStyle(cell, key, value) {
    return GraphUtil.updateStyle(this.graph, cell, key, value)
  }

  // get all cells selected
  getCellsSelected() {
    return this.graph.getSelectionCells()
  }

  /**
   * 从图形中获取XML字符串
   */
  getGraphXml() {
    const xml = GraphUtil.getGraphXml({
      graph: this.graph
    })
    const xmlStr = new XMLSerializer().serializeToString(xml) // eslint-disable-line
    return xmlStr
  }

  /**
   * 读取xml文档
   * @param node
   */
  setGraphXml(node: Element) {
    if (node != null) {
      //@ts-ignore
      const dec = new mxCodec(node.ownerDocument)

      if (node.nodeName == 'mxGraphModel') {
        this.graph.model.beginUpdate()

        try {
          this.graph.model.clear()
          this.graph.view.scale = 1
          this.readGraphState(node)
          this.updateGraphComponents()
          dec.decode(node, this.graph.getModel())
        } finally {
          this.graph.model.endUpdate()
        }
      } else if (node.nodeName == 'root') {
        // Workaround for invalid XML output in Firefox 20 due to bug in mxUtils.getXml
        const wrapper = dec.document.createElement('mxGraphModel')
        wrapper.appendChild(node)

        dec.decode(wrapper, this.graph.getModel())
        this.updateGraphComponents()
      } else {
        message.error('文件格式不正确，不能打开文档')
      }
    } else {
      this.graph.model.clear()
    }
  }

  /**
   * 读取节点状态，更新graph信息
   * @param node
   */
  readGraphState(node: Element) {
    this.graph.gridEnabled = node.getAttribute('grid') != '0'
    //@ts-ignore
    this.graph.gridSize = parseFloat(node.getAttribute('gridSize')) || this.graph.gridSize
    this.graph.graphHandler.guidesEnabled = node.getAttribute('guides') != '0'
    this.graph.setTooltips(node.getAttribute('tooltips') != '0')
    this.graph.setConnectable(node.getAttribute('connect') != '0')
    //@ts-ignore
    this.graph.connectionArrowsEnabled = node.getAttribute('arrows') != '0'
    this.graph.foldingEnabled = node.getAttribute('fold') != '0'

    // if (this.isChromelessView() && this.graph.foldingEnabled) {
    //   this.graph.foldingEnabled = urlParams['nav'] == '1'
    //   this.graph.cellRenderer.forceControlClickHandler = this.graph.foldingEnabled
    // }
    //@ts-ignore
    const ps = parseFloat(node.getAttribute('pageScale'))

    if (!isNaN(ps) && ps > 0) {
      this.graph.pageScale = ps
    } else {
      this.graph.pageScale = mxGraph.prototype.pageScale
    }

    if (!this.graph.isLightboxView() && !this.graph.isViewer()) {
      //@ts-ignore
      const pv = node.getAttribute('page')

      if (pv != null) {
        this.graph.pageVisible = pv != '0'
      } else {
        this.graph.pageVisible = this.graph.defaultPageVisible
      }
    } else {
      this.graph.pageVisible = false
    }

    this.graph.pageBreaksVisible = this.graph.pageVisible
    this.graph.preferPageSize = this.graph.pageBreaksVisible
    //@ts-ignore
    const pw = parseFloat(node.getAttribute('pageWidth'))
    //@ts-ignore
    const ph = parseFloat(node.getAttribute('pageHeight'))

    if (!isNaN(pw) && !isNaN(ph)) {
      this.graph.pageFormat = new mxRectangle(0, 0, pw, ph)
    }

    // Loads the persistent state settings
    const bg = node.getAttribute('background')

    if (bg != null && bg.length > 0) {
      this.graph.background = bg
    } else {
      this.graph.background = ''
    }
  }

  /**
   * 更新graph背景
   */
  updateGraphComponents() {
    const graph = this.graph
    if (graph.container != null) {
      graph.view.validateBackground()
      graph.container.style.overflow = graph.scrollbars ? 'auto' : 'hidden'
    }
  }

  /**
   * 重置Graph属性
   */
  resetGraph() {
    this.graph.timerAutoScroll = true
    this.graph.transparentBackground = false
    this.graph.pageVisible = true
    this.graph.pageFormat = mxConstants.PAGE_FORMAT_A4_LANDSCAPE
    this.graph.setTooltips(true)
    this.graph.setPanning(true)
    //@ts-ignore
    this.graph.collapsedImage = ''
    //@ts-ignore
    this.graph.expandedImage = ''
    this.graph.gridSize = 10
    // 允许连接
    this.graph.setConnectable(true)

    this.graph.gridEnabled = true
    this.graph.graphHandler.guidesEnabled = true
    this.graph.foldingEnabled = true
    this.graph.scrollbars = true
    this.graph.pageVisible = this.graph.defaultPageVisible
    this.graph.pageBreaksVisible = this.graph.pageVisible
    this.graph.preferPageSize = this.graph.pageBreaksVisible
    //@ts-ignore
    this.graph.background = null
    this.graph.pageScale = mxGraph.prototype.pageScale
    this.graph.pageFormat = mxGraph.prototype.pageFormat
    this.graph.currentScale = 1
    this.graph.currentTranslate.x = 0
    this.graph.currentTranslate.y = 0
    this.updateGraphComponents()
    this.graph.view.setScale(1)
  }

  /**
   * 设置滚动条位置
   */
  resetScrollbars() {
    const graph = this.graph

    if (mxUtils.hasScrollbars(graph.container)) {
      if (graph.pageVisible) {
        const pad = graph.getPagePadding()
        graph.container.scrollTop = Math.floor(pad.y) - 1
        graph.container.scrollLeft =
          Math.floor(
            Math.min(pad.x, (graph.container.scrollWidth - graph.container.clientWidth) / 2)
          ) - 1

        // Scrolls graph to visible area
        const bounds = graph.getGraphBounds()

        if (bounds.width > 0 && bounds.height > 0) {
          if (bounds.x > graph.container.scrollLeft + graph.container.clientWidth * 0.9) {
            graph.container.scrollLeft = Math.min(
              bounds.x + bounds.width - graph.container.clientWidth,
              bounds.x - 10
            )
          }

          if (bounds.y > graph.container.scrollTop + graph.container.clientHeight * 0.9) {
            graph.container.scrollTop = Math.min(
              bounds.y + bounds.height - graph.container.clientHeight,
              bounds.y - 10
            )
          }
        }
      } else {
        const bounds = graph.getGraphBounds()
        const width = Math.max(bounds.width, graph.scrollTileSize.width * graph.view.scale)
        const height = Math.max(bounds.height, graph.scrollTileSize.height * graph.view.scale)
        graph.container.scrollTop = Math.floor(
          Math.max(0, bounds.y - Math.max(20, (graph.container.clientHeight - height) / 4))
        )
        graph.container.scrollLeft = Math.floor(
          Math.max(0, bounds.x - Math.max(0, (graph.container.clientWidth - width) / 2))
        )
      }
    } else {
      const b = mxRectangle.fromRectangle(
        graph.pageVisible ? graph.view.getBackgroundPageBounds() : graph.getGraphBounds()
      )
      const tr = graph.view.translate
      const s = graph.view.scale
      b.x = b.x / s - tr.x
      b.y = b.y / s - tr.y
      b.width /= s
      b.height /= s

      const dy = graph.pageVisible ? 0 : Math.max(0, (graph.container.clientHeight - b.height) / 4)

      graph.view.setTranslate(
        Math.floor(Math.max(0, (graph.container.clientWidth - b.width) / 2) - b.x + 2),
        Math.floor(dy - b.y + 1)
      )
    }
  }

  /**
   * create vertex
   * @param {shapeLabel, x, y, width, height, shapeStyle} param0 shapeLabel, x, y, width, height, shapeStyle
   */
  createVertex(shapeLabel, x, y, width, height, shapeStyle) {
    const { graph } = this

    const parent = graph.getDefaultParent()
    const cell = graph.insertVertex(parent, null, shapeLabel, x, y, width, height, shapeStyle)

    return cell
  }

  /**
   * insert edge
   * @param {*} v1 cell 1
   * @param {*} v2 cell 2
   */
  insertEdge(v1, v2) {
    const parent = this.graph.getDefaultParent()

    return this.graph.insertEdge(parent, null, '', v1, v2)
  }

  /**
   * get cell by id
   * @param {*} id id
   */
  getCellById(id) {
    const { cells } = this.graph.model

    let cell

    cells &&
      Object.keys(cells).forEach((key) => {
        if (cells[key].id === id) {
          cell = cells[key]
        }
      })

    return cell
  }

  /**
   * get all cells
   */
  getAllCells() {
    const { cells } = this.graph.model
    return cells
  }

  removeEventListeners() {
    return GraphUtil.removeEventListeners()
  }

  /**
   * rename a cell label
   * @param {*} newName new name
   * @param {*} cell a cell
   */
  renameCell(newName, cell) {
    return GraphUtil.renameCell(newName, cell, this.graph)
  }

  /**
   * refresh the graph
   */
  refresh() {
    return this.graph.refresh()
  }

  /**
   * clear selection in the graph
   */
  clearSelection() {
    return this.graph.clearSelection()
  }

  startPanning() {
    return GraphUtil.startPanning(this.graph)
  }

  stopPanning() {
    return GraphUtil.stopPanning(this.graph)
  }
}
